--[[ LocoBidder 0.3.1 (2009-08-27)
 Developed by Hotspot of Loco Motives guild on EU-Shattered Hand.
 Thanks to Siniava for help with beta testing.
 Thanks to my guild for having patience while testing this addon in raid.
-------------------------------------------------------------------------
 Changelog:
 2009-08-29: Bugfix for master bid.
 2009-08-27: Bugfixes for the priority check. Should be ready for use now.
 2009-08-24: Added "announce" feature. Typing /loco announce will
 echo the current ruleset to the raid.
 2009-08-24: Three new configuration variables
 MinimumBid: Minimum allowed bid in auctions
 EnablePriorityCheck: Bidders can choose a priority for their bid
 (mainspec/offspec). This is done by appending a letter to the bid,
 e.g. '24m' would mean 'bid 24 for mainspec'. The first "letter"
 character in the string is taken as the priority, so any of these
 will work: '24-mainspec', '24/main', '24 Main' but not '24 for mainspec'
 DefaultPriority: The default priority to assume for a bid when
 none is given. It must be one of the defined priorities in this
 source file (currently 'o' or 'm').
 2009-08-24: The variable LocoBidder_Bids now also stores the bids
 together with priority.
 LocoBidder_Bids = {
    User1 = { bid = bid1, priority = priority1 }
    User2 = { bid = bid2, priority = priority2 }
 }
 The priority level has higher importance than the bid. (Used for main>offspec)
 2009-08-08: Instead of giving an error message when a user bids over
 his budget, his bid will now instead be capped at the maximum allowed bid.
 2009-08-08: Removed variable LocoBidder_Items and replaced it with 
 LocoBidder_XmlLog which contains an XML string representing all
 auctions including bids
 2009-05-19: Added an error check for master bid amount.
 2009-05-04: Implemented a system for borrowing DKP. Renamed variables
 called *EQDKP* to *DKP*
 2009-03-27: Fixed bug receiving raid chat messages from raid leader
 2009-03-25: Fixed bug preventing Paladins bidding on Librams
 2009-03-21: Warriors can bid on staves now.
 2009-03-11: Workaround for an issue where melee could not bid on backs
 because they are considered as cloth.
 2009-03-09: Wrote code that checks whether a class can use a
 certain item type, to prevent e.g. mages bidding on plate and
 hunters bidding on maces. Needs testing. Added function
 to raidroll items, /loco raidroll [item]. Betatested in raid
 for the first time and it worked well. Minor bugfixes. Updated the
 PHP script to save data in UTF-8 format.
 2009-03-08: Moved configuration variables to SavedVariables. Added
 slash commands to change configuration variables. /loco config VarName
 (case sensitive). Wrote most of the master loot code, still needs
 more beta testing.
 2009-03-07: Bug fix with handling of /roll events. Added flag for
 disabling dkp. Beta testing. More bug fixes.
 2009-03-06: Added handling of /roll events. Better error reporting for
 invalid bids, added feature for users to get their current DKP
 by whispering "dkp". Added bidding functionality for master bidder
 with /loco masterbid XX
 2009-03-05: First embryo of a working version.
-------------------------------------------------------------------------
 TODO list:
 GUI
 Configuration variables configurable in the UI

 for reference:
 rarity levels:-- 0-gray--1-white--2-green--3-blue--4-epic--5-legendary
-------------------------------------------------------------------------]]

local LocoBidder_Version = "0.3.1";

-- local variables for internal use. don't change
local LocoBidder_time = 0;
local LocoBidder_CurrentInterval = 0;
local LocoBidder_BiddingActive = false;
local LocoBidder_Bids = {};
local LocoBidder_AuctionStartTime = 0;
local LocoBidder_FirstWarned = false;
local LocoBidder_SecondWarned = false;
local LocoBidder_CurrentItem = "";
local LocoBidder_Rolls = {};
local LocoBidder_AuctionActive = false;
local LocoBidder_AwaitingRollsFrom = {};
local LocoBidder_CostOfCurrentItem = 0;
local LocoBidder_MasterBidAmount = 0;
local LocoBidder_MasterbidActive = false;
local LocoBidder_CurrentItemSubType = "";
local LocoBidder_CurrentItemType = "";
local LocoBidder_AwaitingRaidRoll = false;
local LocoBidder_AwaitingConfirmation = false;
local LocoBidder_AuctionConfirmed = false;

local LocoBidder_PriorityMap = { m = 1, o = 0 } -- mainspec 1, offspec 0
local LocoBidder_PriorityString = { m = "Mainspec", o = "Offspec" }

function LocoBidder_OnLoad()
    this:RegisterEvent("CHAT_MSG_WHISPER"); -- to listen for bids in whisper
    this:RegisterEvent("CHAT_MSG_SYSTEM"); -- to listen for rolls
    this:RegisterEvent("CHAT_MSG_RAID"); -- to listen for "pass" messages in raid chat
    this:RegisterEvent("LOOT_OPENED"); -- to link all items in raid chat when loot window is opened
    this:RegisterEvent("VARIABLES_LOADED"); -- to get configuration from SavedVariables
    this:RegisterEvent("RAID_ROSTER_UPDATE"); -- fired when players join or leave the raid
    SLASH_LOCOBIDDER1 = "/locobidder";
    SLASH_LOCOBIDDER2 = "/loco";
    SlashCmdList["LOCOBIDDER"] = LocoBidder_SlashHandler;
end

function LocoBidder_GetCmd(msg)
    if msg then
        local a, b, c = strfind(msg, "(%S+)");
        if a then
            return c, strsub(msg, b + 2);
        else
            return "";
        end
    end
end

function LocoBidder_NormalizeName(name)
    if (string.len(name) > 1) then
        name = string.upper(string.sub(name, 1, 1)) .. string.lower(string.sub(name, 2))
    end
    return name
end

function LocoBidder_SlashHandler(msg)
    if msg then
        local first, SubCmd = LocoBidder_GetCmd(msg);
        if first == "start" then
            local Item = SubCmd;
            LocoBidder_StartAuction(LocoBidder_trim(Item));
            if (LocoBidder_MasterBidActive) then
                LocoBidder_MasterBid(LocoBidder_MasterBidAmount);
            end
        elseif first == "raidroll" then
            local Item = SubCmd;
            LocoBidder_RaidRoll(LocoBidder_trim(Item));
        elseif first == "end" or first == "stop" or first == "cancel" or first == "abort" then
            LocoBidder_CancelAuction();
        elseif first == "adjust" then
            local player, amount = LocoBidder_GetCmd(SubCmd);
            player = LocoBidder_NormalizeName(player)
            if not LocoBidder_AuctionActive then
                LocoBidder_Adjust(player, amount);
            else
                LocoBidder_DisplayMessagePrivate("Error: Cannot adjust while auction is active")
            end
        elseif first == "masterbid" then
            local amount = tonumber(LocoBidder_trim(SubCmd));
            local player_dkp = LocoBidder_GetPlayerMaxBid(UnitName("player"))
            if amount <= player_dkp then
                LocoBidder_MasterBidAmount = amount;
                LocoBidder_MasterBidActive = true;
            else
                LocoBidder_DisplayMessagePrivate("Error: You can only bid up to " .. player_dkp);
                LocoBidder_MasterBidActive = false;
            end
            LocoBidder_DisplayMessagePrivate("Placed master bid of " .. LocoBidder_MasterBidAmount ..
                " for next item");
        elseif first == "wipemasterbid" then
            LocoBidder_MasterBidAmount = 0;
            LocoBidder_MasterBidActive = false;
            LocoBidder_DisplayMessagePrivate("Master bid wiped");
        elseif first == "pass" then -- to pass for a user
            local user = LocoBidder_trim(SubCmd);
            user = LocoBidder_NormalizeName(user)
            if strlen(user) > 0 then
                LocoBidder_Pass(user);
            end
        elseif first == "config" then
            local variable, value = LocoBidder_GetCmd(SubCmd);
            if value and strlen(value) > 0 then
                LocoBidder_UpdateConfigVariable(variable, value);
            elseif variable and strlen(variable) > 0 then
                if not (LocoBidder_Config[variable] == nil) then
                    LocoBidder_DisplayMessagePrivate("Variable " .. variable .. " has value " ..
                        tostring(LocoBidder_Config[variable]) .. " (" .. type(LocoBidder_Config[variable]) .. ")");
                else
                    LocoBidder_DisplayMessagePrivate("No such variable: " .. variable);
                end
            else -- list all configuration variables
                for index, value in pairs(LocoBidder_Config) do
                    LocoBidder_DisplayMessagePrivate(index .. " = " .. tostring(value) .. " (" .. type(value) .. ")");
                end
            end
        elseif first == "announce" then
            LocoBidder_Announce()
        elseif first == "confirm" then
            LocoBidder_ConfirmAuction()
        elseif first == "deny" then
            LocoBidder_DenyAuction()
        else
            LocoBidder_DisplayMessagePrivate("Valid commands: start, end, adjust, masterbid, wipemasterbid, pass, config, raidroll, announce, confirm, deny");
        end
    end
end


function LocoBidder_LogToChannel(logstring)
    if LocoBidder_Config["EnableLogging"] then
        local hour, minute = GetGameTime()
        SendChatMessage('[Log ' .. hour .. ':' .. minute .. '] ' .. logstring, "OFFICER", nil, nil)
    end
end

function LocoBidder_UpdateConfigVariable(variable, value)
    if (LocoBidder_Config[variable] == nil) then
        LocoBidder_DisplayMessagePrivate("No such variable: " .. variable);
    else
        local a,b,c = strfind(value, "^(%d+)$");
        if a then
            value = tonumber(value);
        end
        if (value == "true") then
            value = true;
        elseif (value == "false") then
            value = false;
        end
        if type(value) == type(LocoBidder_Config[variable]) then  -- cannot change type of variable
            LocoBidder_Config[variable] = value;
            LocoBidder_DisplayMessagePrivate("Variable " .. variable .. " changed to " .. tostring(value) .. " (" .. type(value) .. ")");
        else
            LocoBidder_DisplayMessagePrivate("Error: Variable " .. variable .. " must be of type " .. type(LocoBidder_Config[variable]) .. ".")
        end
    end
end

function LocoBidder_Pass(user)
    local validPass = false
    local removeIndex = 0;
    for index, value in pairs(LocoBidder_AwaitingRollsFrom) do
        if (value == user) then
            validPass = true;
            removeIndex = index;
            break;
        end
    end
    if validPass then
        table.remove(LocoBidder_AwaitingRollsFrom, removeIndex);
        LocoBidder_SendWhisper("You have passed on " .. LocoBidder_CurrentItem .. ".", user);
    end
    if table.getn(LocoBidder_AwaitingRollsFrom) == 0 then -- not awaiting any more rolls
        LocoBidder_AuctionRollWrapUp();
    elseif table.getn(LocoBidder_AwaitingRollsFrom) == 1 then
        -- the one person that has not rolled is the winner if LocoBidder_Rolls is empty
        local existRolls = false;
        for i, v in pairs(LocoBidder_Rolls) do
            if (v > 0) then
                existRolls = true;
                break;
            end
        end
        if not(existRolls) then -- everyone has passed
            local winner = LocoBidder_AwaitingRollsFrom[1];
            LocoBidder_DisplayMessage(winner .. " wins " .. LocoBidder_CurrentItem .. " for " ..
                LocoBidder_CostOfCurrentItem .. ". (Others passed)");
            LocoBidder_Deduct(winner, LocoBidder_CostOfCurrentItem);
            LocoBidder_AssignItem(winner);
            LocoBidder_EndCurrentAuction();
        end
    end
end

function LocoBidder_MasterBid(bid)
    local playerName = UnitName("player")
    local priority = 'm'
    bid = math.abs(math.floor(bid + 0.5))
    LocoBidder_Bids[playerName] = { bid = bid, priority = priority }
    LocoBidder_DisplayMessage("The master looter has placed a bid in advance for this item.")
end

function LocoBidder_StartAuction(itemLink)
    if not(LocoBidder_AuctionActive) then
        if strlen(itemLink) > 0 then
            LocoBidder_DisplayMessage("Auction for '" .. itemLink .. "' has started. "..
                "Whisper me your bids now! [LocoBidder " .. LocoBidder_Version .. "]");
            LocoBidder_BiddingActive = true;
            LocoBidder_AuctionActive = true;
            LocoBidder_FirstWarned = false;
            LocoBidder_SecondWarned = false;
            LocoBidder_AuctionStartTime = math.floor(GetTime());
            LocoBidder_CurrentItem = itemLink;
            local itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType = GetItemInfo(itemLink);
            LocoBidder_CurrentItemType = itemType;
            LocoBidder_CurrentItemSubType = itemSubType;
            LocoBidder_Bids = {};
            LocoBidder_Rolls = {};
            if itemName == nil then
                itemName = LocoBidder_CurrentItem
            end
            LocoBidder_XmlLog = LocoBidder_XmlLog .. '<auction item="' .. itemName .. '">';
        else
            LocoBidder_DisplayMessagePrivate("No item specified");
        end
    else
        LocoBidder_DisplayMessagePrivate("An auction is active. End it first.");
    end
end

function LocoBidder_DisplayMessage(msg)
    if strlen(msg) > 0 then
        if (LocoBidder_Config["Sharp"]) then
            SendChatMessage("" .. msg, "RAID", LocoBidder_Config["BiddingLanguage"]);
        else
            DEFAULT_CHAT_FRAME:AddMessage("" .. msg);
        end
    end
end

function LocoBidder_DisplayMessagePrivate(msg)
    if msg then
        DEFAULT_CHAT_FRAME:AddMessage("LocoBidder: " .. msg)
    end
end

function LocoBidder_trim(s)
    return (string.gsub(s, "^%s*(.-)%s*$", "%1"));
end

function LocoBidder_SendWhisper(str, player)
    if not(player == UnitName("player")) then
        SendChatMessage(str, "WHISPER", LocoBidder_Config["BiddingLanguage"], player);
    else -- don't actually whisper yourself, display a message instead.
        LocoBidder_DisplayMessagePrivate(str);
    end
end

function LocoBidder_GetPlayerDKP(player)
    local user_dkp = 0;
    if (LocoBidder_Config["Use_DKP"]) then
        if not(LocoBidder_DKP[player] == nil) then
            user_dkp = math.floor(tonumber(LocoBidder_DKP[player]) + 0.5);
        end
    end
    return user_dkp;
end

function LocoBidder_GetPlayerLoaned(player)
    local user_loaned = 0;
    if (LocoBidder_Config["Use_DKP"] and LocoBidder_Config["EnableLoans"]) then
        if not(LocoBidder_Loaned[player] == nil) then
            user_loaned = math.floor(tonumber(LocoBidder_Loaned[player]) + 0.5);
        end
    end
    return user_loaned;
end

function LocoBidder_GetPlayerCredit(player) -- how much he still can loan
    local user_credit = 0;
    if (LocoBidder_Config["Use_DKP"] and LocoBidder_Config["EnableLoans"]) then
        local user_loaned = LocoBidder_GetPlayerLoaned(player);
        user_credit = LocoBidder_Config["RaidBaseValue"] - user_loaned;
    end
    return user_credit;
end

function LocoBidder_GetPlayerMaxBid(player) -- maximum bid that a player can place is:  max_bid = dkp + (base_value - loaned)
    local user_dkp = LocoBidder_GetPlayerDKP(player);
    if not(LocoBidder_Config["EnableLoans"]) then
        return user_dkp;
    end
    local user_loaned = LocoBidder_GetPlayerLoaned(player);
    return (user_dkp + math.floor(tonumber(LocoBidder_Config["RaidBaseValue"]) + 0.5) - user_loaned);
end

function LocoBidder_GetPlayerSpent(player)
    local user_spent = 0;
    if not(LocoBidder_Spent[player] == nil) then
        user_spent = math.floor(tonumber(LocoBidder_Spent[player]) + 0.5);
    end
    return user_spent;
end

function LocoBidder_OnEvent()
    if (event == "CHAT_MSG_WHISPER") then
        text = LocoBidder_trim(string.lower(arg1));
        player = arg2;
        if UnitInRaid("player") and UnitInRaid(player) then -- if I am in raid AND the whisperer is in (the same) raid, then...
            if (text == "dkp" and LocoBidder_Config["Use_DKP"]) then
                local user_dkp = LocoBidder_GetPlayerDKP(player);
                local user_spent = LocoBidder_GetPlayerSpent(player);
                local result_line = "";
                result_line = "[ DKP: " .. user_dkp .. " ][ Spent: " .. user_spent .. " ]";
                if LocoBidder_Config["EnableLoans"] then
                    local user_loaned = LocoBidder_GetPlayerLoaned(player);
                    local user_max_bid = LocoBidder_GetPlayerMaxBid(player);
                    local user_credit = LocoBidder_GetPlayerCredit(player);
                    result_line = result_line .. "[ Credit: " .. user_credit .. " ][ Max Bid: " .. user_max_bid .. " ]";
                end
                LocoBidder_SendWhisper(result_line, player);
            elseif (text == "cancel") and LocoBidder_BiddingActive then
                -- Check if user has already put a bid
                local validCancel = false;
                for index, value in pairs(LocoBidder_Bids) do
                    if (index == player) then
                        validCancel = true;
                        break;
                    end
                end
                if not(validCancel) then
                    LocoBidder_SendWhisper("Error: You have not yet placed a bid.", player);
                else
                    LocoBidder_Bids[player] = nil;
                    LocoBidder_SendWhisper("Bid cancelled.", player);
                end
            elseif LocoBidder_BiddingActive then
                local whisper = ""
                local a, b, c = string.find(text, "(%d+)") -- see if it contains a number
                local unitClass = UnitClass(player)
                if not (a == nil) then -- it is a number
                    local user_bid = tonumber(c)
                    local user_max_bid = LocoBidder_GetPlayerMaxBid(player)
                    local priority
                    if LocoBidder_Config["EnablePriorityCheck"] then
                        -- get priority from whisper. the first letter in the string is the requested priority            
                        local a, b, c = string.find(text, "(%a)")
                        if not (a == nil) then -- a letter was found...
                            -- c is now the requested priority. accept if it is in the priority map
                            c = strlower(c)
                            if not(LocoBidder_PriorityMap[c] == nil) then
                                priority = c
                            else -- undefined priority - use default
                                priority = LocoBidder_Config["DefaultPriority"]
                            end
                        else -- no requested priority - use default
                            priority = LocoBidder_Config["DefaultPriority"]
                        end
                    else -- if the priority check is not enabled, everybody gets the same priority
                        priority = LocoBidder_Config["DefaultPriority"]
                    end
                    if LocoBidder_Config["Use_DKP"] and user_max_bid < user_bid then -- check if user has enough DKP
                        -- cap at max bid, but only if it is greater than the minimum allowed bid
                        if user_max_bid >= LocoBidder_Config["MinimumBid"] then
                            whisper = "Notice: You tried to bid over your budget. " ..
                                "Bid capped at " .. user_max_bid .. ". Type ''cancel'' to remove your bid."
                            if LocoBidder_Config["EnablePriorityCheck"] then
                                whisper = whisper .. " Priority: " .. LocoBidder_PriorityString[priority] .. "."
                            end
                            LocoBidder_Bids[player] = { bid = user_max_bid, priority = priority }
                        else
                            whisper = "Error: You tried to bid over your budget and you cannot afford the minimum bid of " ..
                                LocoBidder_Config["MinimumBid"] .. "."
                        end
                    elseif not (math.abs(math.floor(user_bid + 0.5)) == user_bid) then -- check if the number is a positive integer
                        whisper = "Error: You tried to bid " .. user_bid ..
                            ", but a bid must be a positive whole number or zero. Example of valid bids: " ..
                            "0, 1, 2... Bid not accepted. Try again."
                    elseif not (LocoBidder_IsAllowed(unitClass, LocoBidder_CurrentItemSubType)) then
                        whisper = "Error: You tried to bid on an item of type ''" .. LocoBidder_CurrentItemSubType ..
                            "'' but your class ''" .. unitClass .. "'' cannot use (or benefit from) this item."
                    elseif user_bid < LocoBidder_Config["MinimumBid"] then -- user bidded lower than the minimum bid
                        -- distinguish two cases: 1) he can afford the minimum bid, or 2) he cannot afford the minimum bid
                        if user_max_bid >= LocoBidder_Config["MinimumBid"] then -- 1)
                            -- change his bid to MinimumBid and accept
                            whisper = "Notice: Minimum bid is " .. LocoBidder_Config["MinimumBid"] ..
                                ". Your bid has been changed to this amount. Type ''cancel'' to remove your bid."
                                if LocoBidder_Config["EnablePriorityCheck"] then
                                    whisper = whisper .. " Priority: " .. LocoBidder_PriorityString[priority] .. "."
                                end
                            LocoBidder_Bids[player] = { bid = LocoBidder_Config["MinimumBid"], priority = priority }
                        else -- 2)
                            -- sry m8, no soup for you
                            LocoBidder_SendWhisper("Error: Sorry, the minimum bid is " .. LocoBidder_Config["MinimumBid"] ..
                                " and you do not have enough credit to bid this amount.")
                        end
                    else
                        whisper = "Bid of " .. user_bid .. " accepted. You can bid up to " .. user_max_bid .. "."
                        if LocoBidder_Config["EnablePriorityCheck"] then
                            whisper = whisper .. " Priority: " .. LocoBidder_PriorityString[priority] .. "."
                        end
                        LocoBidder_Bids[player] = { bid = user_bid, priority = priority }
                    end
                else -- not a number
                    whisper = "Error: Invalid bid. The bid must be a positive whole number" ..
                        " (example: 0, 1, 2, ...). Please try again."
                end
                LocoBidder_SendWhisper(whisper, player)
            end
        end
    elseif (event == "CHAT_MSG_SYSTEM") then -- to track rolls
        -- important to keep the 1-100 so rollers can't cheat with e.g. /roll 100-100.
        -- note that the character "-" has a special meaning in lua patterns, as opposed to regexps in perl/php
        local a, b, user, roll = strfind(arg1, "^([^%s]+) rolls (%d+)%s%(1%-100%)$");
        if (LocoBidder_AuctionActive and not (a == nil)) then  -- auction is active and a roll was received
            roll = tonumber(roll);
            -- check if awaiting roll from user
            local validRoll = false;
            local removeIndex = 0;
            for index, value in pairs(LocoBidder_AwaitingRollsFrom) do
                if (value == user) then
                    validRoll = true;
                    removeIndex = index;
                    break;
                end
            end
            -- remove user from list of awaiting rolls
            if validRoll then
                table.remove(LocoBidder_AwaitingRollsFrom, removeIndex);
                LocoBidder_Rolls[user] = roll;
            end
            -- if not awaiting more rolls, wrap up auction
            if table.getn(LocoBidder_AwaitingRollsFrom) == 0 then
                LocoBidder_AuctionRollWrapUp();
            end
        end
        -- check for RaidRolls
        a, b, user, roll = strfind(arg1, "^([^%s]+) rolls (%d+)%s");
        if (LocoBidder_AwaitingRaidRoll) and user == UnitName("player") then
            roll = tonumber(roll);
            local counter = 1;
            for i = 1, 40 do -- TODO change 40 to the constant defined by WoW API
                local unitID = "raid" .. tostring(i);
                if (UnitInRaid(unitID)) then
                    if (counter == roll) then
                        local winner = UnitName(unitID);
                        LocoBidder_DisplayMessage(winner .. " wins " .. LocoBidder_CurrentItem);
                        LocoBidder_AssignItem(winner);
                        break;
                    end
                    counter = counter + 1;
                end
            end
            LocoBidder_AwaitingRaidRoll = false;
        end
    elseif (event == "CHAT_MSG_RAID") or (event == "CHAT_MSG_RAID_LEADER") then -- check for "pass" messages in raid chat
        if (LocoBidder_AuctionActive) then
            chatMsg = strlower(LocoBidder_trim(arg1))
            if (chatMsg == 'pass' or chatMsg == 'pass!' or chatMsg == 'pass.' or chatMsg == 'ta den' or
                chatMsg == 'gz' or chatMsg == 'jag passar' or chatMsg == 'gz!' or chatMsg == 'ta den du') then
                user = arg2;
                LocoBidder_Pass(user);
            end
        end
    elseif (event == "LOOT_OPENED") then -- print the loot in raid chat
        if (UnitInRaid("player")) then -- only if in raid group
            for index = 1, GetNumLootItems() do
                if LootSlotIsCoin(index) then -- autoloot gold
                    -- TODO: put this in a config variable - an addon should not have hidden functionality like this
                    --LootSlot(index);
                elseif (LootSlotIsItem(index)) then
                    local itemLink = GetLootSlotLink(index);
                    local itemName, itemLink, itemQuality, itemLevel = GetItemInfo(itemLink);
                    if (itemQuality >= LocoBidder_Config["RarityThreshold"] and itemLevel >= LocoBidder_Config["MinItemLevel"]) then
                        LocoBidder_DisplayMessage("Drop: " .. itemLink .. " (iLevel " .. itemLevel .. ")");
                    end
                end
            end
        end
    elseif (event == "VARIABLES_LOADED") then
        if (LocoBidder_Config == nil) then
            LocoBidder_Config = {};
        end
        if (LocoBidder_Spent == nil) then
            LocoBidder_Spent = {};
        end
        if (LocoBidder_DKP == nil) then
            LocoBidder_DKP = {};
        end
        if (LocoBidder_Loaned == nil) then
            LocoBidder_Loaned = {};
        end
        if (LocoBidder_Roster == nil) then
            LocoBidder_Roster = {};
        end
        if (LocoBidder_XmlLog == nil) then
            LocoBidder_XmlLog = "";
        end
        -- provide default config values
        if (LocoBidder_Config["Sharp"] == nil) then LocoBidder_Config["Sharp"] = true; end
        if (LocoBidder_Config["BiddingLanguage"] == nil) then LocoBidder_Config["BiddingLanguage"] = "ORCISH"; end
        if (LocoBidder_Config["FirstWarning"] == nil) then LocoBidder_Config["FirstWarning"] = 8; end
        if (LocoBidder_Config["SecondWarning"] == nil) then LocoBidder_Config["SecondWarning"] = 12; end
        if (LocoBidder_Config["AuctionDuration"] == nil) then LocoBidder_Config["AuctionDuration"] = 15; end
        if (LocoBidder_Config["Use_DKP"] == nil) then LocoBidder_Config["Use_DKP"] = true; end
        if (LocoBidder_Config["UpdateInterval"] == nil) then LocoBidder_Config["UpdateInterval"] = 50; end
        if (LocoBidder_Config["MinItemLevel"] == nil) then LocoBidder_Config["MinItemLevel"] = 213; end
        if (LocoBidder_Config["RarityThreshold"] == nil) then LocoBidder_Config["RarityThreshold"] = 4; end
        if (LocoBidder_Config["Disenchanter"] == nil) then LocoBidder_Config["Disenchanter"] = ""; end
        if (LocoBidder_Config["EnableLoans"] == nil) then LocoBidder_Config["EnableLoans"] = false; end
        if (LocoBidder_Config["RaidBaseValue"] == nil) then LocoBidder_Config["RaidBaseValue"] = 20; end
        if (LocoBidder_Config["MinimumBid"] == nil) then LocoBidder_Config["MinimumBid"] = 0; end
        if (LocoBidder_Config["EnablePriorityCheck"] == nil) then LocoBidder_Config["EnablePriorityCheck"] = false; end
        if (LocoBidder_Config["DefaultPriority"] == nil) then LocoBidder_Config["DefaultPriority"] = 'o'; end
        if (LocoBidder_Config["ConfirmAuctions"] == nil) then LocoBidder_Config["ConfirmAuctions"] = false; end
        if (LocoBidder_Config["EnableLogging"] == nil) then LocoBidder_Config["EnableLogging"] = true; end
    elseif (event == "RAID_ROSTER_UPDATE") then
        -- loop over raid members
        for i = 1, 40 do
            local unitID = "raid" .. tostring(i);
            if (UnitInRaid(unitID)) then
                local unit_name = UnitName(unitID);
                -- add unit_name to roster if not already present
                local in_roster = false;
                if not(LocoBidder_Roster == nil) then
                    for i, v in pairs(LocoBidder_Roster) do
                        if (v == unit_name) then
                            in_roster = true;
                            break;
                        end
                    end
                end
                if not(in_roster) then
                    table.insert(LocoBidder_Roster, unit_name);
                end
            end
        end
    end
end

function LocoBidder_AuctionRollWrapUp()
    local max_roll = -1;
    local n_max_rolls = 0;
    local max_rollers = {};
    for curRoller, curRoll in pairs(LocoBidder_Rolls) do
        if (curRoll > max_roll) then
            max_roll = curRoll;
            n_max_rolls = 1;
            max_rollers = {};
            table.insert(max_rollers, curRoller);
        elseif (curRoll == max_roll) then
            n_max_rolls = n_max_rolls + 1;
            table.insert(max_rollers, curRoller);
        end
    end
    if n_max_rolls == 1 then
        LocoBidder_DisplayMessage(max_rollers[1] .. " wins " .. LocoBidder_CurrentItem .. " for " .. LocoBidder_CostOfCurrentItem .. ".");
        LocoBidder_Deduct(max_rollers[1], LocoBidder_CostOfCurrentItem);
        LocoBidder_AssignItem(max_rollers[1]);
        LocoBidder_EndCurrentAuction();
    elseif n_max_rolls > 1 then
        LocoBidder_DisplayMessage("Multiple winning rolls.");
        local result_line = "";
        LocoBidder_AwaitingRollsFrom = {};
        for index, value in pairs(max_rollers) do
            result_line = result_line .. "[ " .. value .. " ]";
            table.insert(LocoBidder_AwaitingRollsFrom, value);
        end
        result_line = result_line .. " please re-roll now!";
        LocoBidder_DisplayMessage(result_line);
    end
end

function LocoBidder_OnUpdate()
    if LocoBidder_BiddingActive then
        LocoBidder_CurrentInterval = LocoBidder_CurrentInterval + 1;
        if (LocoBidder_CurrentInterval > LocoBidder_Config["UpdateInterval"]) then
            LocoBidder_CurrentInterval = 0;
            LocoBidder_time = math.floor(GetTime() - LocoBidder_AuctionStartTime);
            if not (LocoBidder_FirstWarned) and LocoBidder_time > LocoBidder_Config["FirstWarning"] then
                LocoBidder_DisplayMessage("Auction will expire in " .. (LocoBidder_Config["AuctionDuration"] -
                    LocoBidder_Config["FirstWarning"]) .. " seconds!");
                LocoBidder_FirstWarned = true;
            elseif not (LocoBidder_SecondWarned) and LocoBidder_time > LocoBidder_Config["SecondWarning"] then
                LocoBidder_DisplayMessage("Auction will expire in " .. (LocoBidder_Config["AuctionDuration"] -
                    LocoBidder_Config["SecondWarning"]) .. " seconds!");
                LocoBidder_SecondWarned = true;
            elseif LocoBidder_time > LocoBidder_Config["AuctionDuration"] then
                LocoBidder_BiddingActive = false;
                LocoBidder_AuctionExpire();
            end
        end
    elseif LocoBidder_AwaitingConfirmation then
        if LocoBidder_AuctionConfirmed then
            LocoBidder_AwaitingConfirmation = false
            LocoBidder_AuctionConfirmed = false
            LocoBidder_AuctionFinished()
        end
    end
end

function LocoBidder_AuctionExpire()
    if LocoBidder_Config["ConfirmAuctions"] then
        LocoBidder_AwaitingConfirmation = true
        LocoBidder_DisplayMessagePrivate("Awaiting confirmation of auction. Type ''/loco confirm'' to confirm or ''/loco deny'' to deny.")
    else
        LocoBidder_AuctionFinished() -- TODO pass as arguments: assign to whom and how much it cost
    end
end

function LocoBidder_ConfirmAuction()
    if LocoBidder_AwaitingConfirmation then
        LocoBidder_AuctionConfirmed = true
        -- the function LocoBidder_OnUpdate will notice that the state changed
    else
        LocoBidder_DisplayMessagePrivate("There are no open auctions to confirm.")
    end
end

function LocoBidder_DenyAuction()
    if LocoBidder_AwaitingConfirmation then
        LocoBidder_CancelAuction()
    else
        LocoBidder_DisplayMessagePrivate("There are no open auctions to deny.")
    end
end

function LocoBidder_AuctionFinished()
    LocoBidder_XmlLog = LocoBidder_XmlLog .. '<bids>';
    -- build an array for each priority level, something like this:
    -- bidArray = { 0: { User1 = 20, User2 = 30 }, 1: { User3 = 10, User4 = 5 } }
    local bidArray = {}
    local maxPriority = -1
    local maxPriorityId = ''
    for curBidder, curBidArray in pairs(LocoBidder_Bids) do
        curBid = curBidArray.bid
        curPriority = LocoBidder_PriorityMap[curBidArray.priority]
        if bidArray[curPriority] == nil then
            bidArray[curPriority] = {}
        end
        if curPriority > maxPriority then
            maxPriority = curPriority
            maxPriorityId = curBidArray.priority
        end
        bidArray[curPriority][curBidder] = curBid
        LocoBidder_XmlLog = LocoBidder_XmlLog .. '<bid name="' .. curBidder ..
            '" value="' .. curBid .. '" priority="' .. LocoBidder_PriorityString[curBidArray.priority] .. '" />'
    end
    LocoBidder_XmlLog = LocoBidder_XmlLog .. '</bids>';

    -- find highest and next highest bid in max priority array
    --

    local max_bid = -1;
    local n_max_bids = 0;
    local max_bidders = {};

    local next_highest_bid = -1;
    local next_highest_bidders = {};
    local n_next_highest_bids = 0;
    
    if maxPriority > -1 then -- (otherwise there are no bids)
        -- find max bid in array
        for curBidder, curBid in pairs(bidArray[maxPriority]) do
            if (curBid > max_bid) then
                max_bid = curBid;
                n_max_bids = 1;
                max_bidders = {};
                table.insert(max_bidders, curBidder);
            elseif (curBid == max_bid) then
                n_max_bids = n_max_bids + 1;
                table.insert(max_bidders, curBidder);
            end
        end
        -- find next highest bid
        for curBidder, curBid in pairs(bidArray[maxPriority]) do
            if (curBid > next_highest_bid and curBid < max_bid) then
                next_highest_bid = curBid;
                n_next_highest_bids = 1;
                next_highest_bidders = {};
                table.insert(next_highest_bidders, curBidder);
            elseif (curBid == next_highest_bid) then
                n_next_highest_bids = n_next_highest_bids + 1;
                table.insert(next_highest_bidders, curBidder);
            end
        end
    end

    if (next_highest_bid == -1) then
        next_highest_bid = 0;
    end

    -- analyze bidding results
    local result_line = "";
    if (n_max_bids == 0) then
        LocoBidder_DisplayMessage("No bids for " .. LocoBidder_CurrentItem);
        -- disenchant. but only if a disenchanter is specified and the disenchanter is in the raid group.
        local disser = tostring(LocoBidder_Config["Disenchanter"]);
        if (strlen(disser) > 0) and UnitInRaid(disser) then
            if (LocoBidder_CurrentItemType == "Armor") or (LocoBidder_CurrentItemType == "Weapon") then -- if disenchantable
                LocoBidder_SendWhisper("Please disenchant " .. LocoBidder_CurrentItem .. ".", disser);
                LocoBidder_AssignItem(disser);
            end
        end
        LocoBidder_EndCurrentAuction();
    else
        if (n_max_bids == 1) then
            local n_show = 2;
            if (n_next_highest_bids == 0) then
                n_show = 1;
            end
            LocoBidder_DisplayMessage("One winning "  .. LocoBidder_PriorityString[maxPriorityId] .. " bid for " .. LocoBidder_CurrentItem .. ". Showing top " .. n_show .. " bids.");
            LocoBidder_DisplayMessage("1. [ " .. max_bidders[1] .. " ] (" .. max_bid .. ")");
            if (n_next_highest_bids > 0) then
                result_line = "2. ";
                for index, value in pairs(next_highest_bidders) do
                    result_line = result_line .. "[ " .. value .. " ]";
                end
                result_line = result_line .. " (" .. next_highest_bid .. ")";
                LocoBidder_DisplayMessage(result_line);
            end
            LocoBidder_DisplayMessage(max_bidders[1] .. " wins " .. LocoBidder_CurrentItem .. " for " .. next_highest_bid .. ".");
            LocoBidder_Deduct(max_bidders[1], next_highest_bid);
            LocoBidder_AssignItem(max_bidders[1]);
            LocoBidder_EndCurrentAuction();
        else
            LocoBidder_DisplayMessage(LocoBidder_NumToText(n_max_bids) .. " winning " .. LocoBidder_PriorityString[maxPriorityId] .. " bids (" .. max_bid .. ") for " .. LocoBidder_CurrentItem .. ".")
            result_line = "";
            for index, value in pairs(max_bidders) do
                result_line = result_line .. "[ " .. value .. " ]";
                table.insert(LocoBidder_AwaitingRollsFrom, value);
            end
            LocoBidder_DisplayMessage(result_line .. " please roll now!");
            LocoBidder_CostOfCurrentItem = max_bid;
        end
    end
end

function LocoBidder_EndCurrentAuction()
    -- LocoBidder_DisplayMessagePrivate("Auction ended");
    LocoBidder_time = 0;
    LocoBidder_CurrentInterval = 0;
    LocoBidder_BiddingActive = false;
    LocoBidder_Bids = {};
    LocoBidder_FirstWarned = false;
    LocoBidder_SecondWarned = false;
    LocoBidder_CurrentItem = "";
    LocoBidder_Rolls = {};
    LocoBidder_AuctionActive = false;
    LocoBidder_AwaitingRollsFrom = {};
    LocoBidder_CostOfCurrentItem = 0;
    LocoBidder_MasterBidAmount = 0;
    LocoBidder_MasterBidActive = false;
    LocoBidder_CurrentItemType = "";
    LocoBidder_CurrentItemSubType = "";
    LocoBidder_AwaitingConfirmation = false;
    LocoBidder_XmlLog = LocoBidder_XmlLog .. '</auction>'
end

function LocoBidder_Deduct(player, amount)
    LocoBidder_Adjust(player, -amount);
end

function LocoBidder_AssignItem(player)
    local item = GetItemInfo(LocoBidder_CurrentItem)
    LocoBidder_LogToChannel('assigned to ' .. player .. ': ' .. item .. ' for ' .. LocoBidder_CostOfCurrentItem .. '.')
    if (GetLootMethod() == "master") then
        local itemName_CurrentItem = GetItemInfo(LocoBidder_CurrentItem);
        local masterLootAssigned = false;
        for ci = 1, GetNumRaidMembers() do
            if (GetMasterLootCandidate(ci) == player) then
                for lootIndex = 1, GetNumLootItems() do
                    if (LootSlotIsItem(lootIndex)) then
                        local itemLink = GetLootSlotLink(lootIndex);
                        local itemName = GetItemInfo(itemLink);
                        if not masterLootAssigned and (itemName == itemName_CurrentItem) then
                            GiveMasterLoot(lootIndex, ci);
                            masterLootAssigned = true;
                            return; -- just in case, so that multiple items with the same name aren't assigned to the same player
                        end
                    end
                end
            end
        end
    end
end

function LocoBidder_Adjust(player, amount)
    amount = tonumber(amount);
    LocoBidder_XmlLog = LocoBidder_XmlLog .. '<adjustment name="' .. player ..
        '" value="' .. amount .. '" />'
    local user_dkp = LocoBidder_GetPlayerDKP(player);
    local user_spent = LocoBidder_GetPlayerSpent(player);
    if not(amount == 0) then
        LocoBidder_DKP[player] = user_dkp + amount;
        LocoBidder_Spent[player] = user_spent - amount;
        if LocoBidder_Config["EnableLoans"] then
            if LocoBidder_DKP[player] < 0 then
                local amount = -LocoBidder_DKP[player];
                -- amount > 0
                -- we must make sure that loaned does not exceed base value in the end
                if LocoBidder_Loaned[player] + amount > LocoBidder_Config["RaidBaseValue"] then
                    amount = LocoBidder_Config["RaidBaseValue"] - LocoBidder_Loaned[player];
                end
                if amount < 0 then
                    amount = 0;
                end
                LocoBidder_DKP[player] = LocoBidder_DKP[player] + amount; -- set it to zero
                 -- DKP and Loaned must be changed by an equal amount to keep max bid constant
                LocoBidder_Loaned[player] = LocoBidder_Loaned[player] + amount;
                 -- happens when manually adjusting after a player has taken a loan
            elseif LocoBidder_DKP[player] > 0 and LocoBidder_Loaned[player] > 0 then
                local amount = LocoBidder_Loaned[player];
                -- must keep dkp >= 0
                if amount > LocoBidder_DKP[player] then
                    amount = LocoBidder_DKP[player];
                end
                LocoBidder_DKP[player] = LocoBidder_DKP[player] - amount;
                LocoBidder_Loaned[player] = LocoBidder_Loaned[player] - amount;
            end
        end
        LocoBidder_SendWhisper("Your points have been adjusted with " .. amount ..
            ". You now have " .. (LocoBidder_GetPlayerDKP(player)) .. ".", player);
    end
end

function LocoBidder_CancelAuction()
    LocoBidder_DisplayMessage("The auction for " .. LocoBidder_CurrentItem ..
        " was cancelled by the master looter.");
    LocoBidder_XmlLog = LocoBidder_XmlLog .. '<cancelled />';
    LocoBidder_EndCurrentAuction();
end

function LocoBidder_IsAllowed(class, itemSubType) -- assume Allowed by default. only unallowed if explicitly defined
    local Unallowed = {};
    Unallowed["Mage"] = { "Leather", "Mail", "Plate", "Two-Handed Swords", "One-Handed Axes", "One-Handed Maces", "Two-Handed Maces", "Two-Handed Axes", "Polearms", "Guns", "Crossbows", "Bows", "Fist Weapons", "Shields", "Librams", "Idols", "Totems", "Thrown" };
    Unallowed["Warlock"] = { "Leather", "Mail", "Plate", "Two-Handed Swords", "One-Handed Axes", "One-Handed Maces", "Two-Handed Maces", "Two-Handed Axes", "Polearms", "Guns", "Crossbows", "Bows", "Fist Weapons", "Shields", "Librams", "Idols", "Totems", "Thrown" };
    Unallowed["Priest"] = { "Leather", "Mail", "Plate", "Two-Handed Swords", "One-Handed Axes", "One-Handed Swords", "Two-Handed Maces", "Two-Handed Axes", "Polearms", "Guns", "Crossbows", "Bows", "Fist Weapons", "Shields", "Librams", "Idols", "Totems", "Thrown" };
    Unallowed["Druid"] = { "Mail", "Plate", "Two-Handed Axes", "One-Handed Axes", "Guns", "Crossbows", "Bows", "Shields", "Librams", "Totems", "Thrown", "Wands" };
    Unallowed["Rogue"] = { "Mail", "Plate", "Two-Handed Axes", "Two-Handed Swords", "Two-Handed Maces", "Polearms", "Staves", "Shields", "Librams", "Idols", "Totems", "Wands" };
    Unallowed["Hunter"] = { "Plate", "One-Handed Maces", "Two-Handed Maces", "Shields", "Librams", "Idols", "Totems", "Wands" };
    Unallowed["Shaman"] = { "Plate", "Polearms", "Guns", "Crossbows", "Bows", "Librams", "Idols", "Thrown", "Wands" };
    Unallowed["Warrior"] = { "Librams", "Idols", "Totems", "Wands" };
    Unallowed["Paladin"] = { "Staves", "Guns", "Crossbows", "Bows", "Idols", "Totems", "Thrown", "Wands" };
    Unallowed["Death Knight"] = { "Staves", "Fist Weapons", "Guns", "Crossbows", "Bows", "Shields", "Librams", "Idols", "Totems", "Thrown", "Wands" };
    if Unallowed[class] ~= nil then
        for i, v in pairs(Unallowed[class]) do
            if v == itemSubType then
                return false;
            end
        end
    end
    return true;
end

function LocoBidder_RaidRoll(itemLink)
    LocoBidder_DisplayMessage("Rolling out: " .. itemLink .. " [LocoBidder " .. LocoBidder_Version .. "]");
    LocoBidder_CurrentItem = itemLink;
    LocoBidder_AwaitingRaidRoll = true;
    RandomRoll(1, GetNumRaidMembers()); -- triggers event CHAT_MSG_SYSTEM
end

function LocoBidder_FreeRoll(itemLink)
    -- TODO
end

function LocoBidder_NumToText(num)
    if num == 1 then
        return "One"
    elseif num == 2 then
        return "Two"
    elseif num == 3 then
        return "Three"
    elseif num == 4 then
        return "Four"
    elseif num == 5 then
        return "Five"
    elseif num == 6 then
        return "Six"
    else
        return "Multiple"
    end
end

function LocoBidder_Announce()
    local line = ""
    line = "=== Welcome to LocoBidder " .. LocoBidder_Version .. " ==="
    LocoBidder_DisplayMessage(line)
    line = "Minimum bid is [ " .. LocoBidder_Config["MinimumBid"] .. " ]"
    LocoBidder_DisplayMessage(line)
    line = "Loans are [ "
    if LocoBidder_Config["EnableLoans"] then
        line = line .. "enabled ] and you can borrow up to [ " .. LocoBidder_Config["RaidBaseValue"] .. " ]"
    else
        line = line .. "disabled ]"
    end
    LocoBidder_DisplayMessage(line)
    if LocoBidder_Config["EnablePriorityCheck"] then
        line = "Bidding priority is [ enabled ] and valid priorities are: "
        for i, v in pairs(LocoBidder_PriorityString) do
            line = line .. "[ " .. v .. "(" .. i .. ") ]"
        end
        LocoBidder_DisplayMessage(line)
        line = "To place a bid with priority, put the priority after your bid, for example: '40m'"
        LocoBidder_DisplayMessage(line)
        line = "If you do not specify a priority, the default is [ " .. LocoBidder_PriorityString[LocoBidder_Config["DefaultPriority"]] .. " ]"
        LocoBidder_DisplayMessage(line)
    else
        line = "Bidding priority is [ disabled ]"
        LocoBidder_DisplayMessage(line)
    end
end

